package cn.dansj.common.utils.spring.request;

import java.io.Closeable;
import java.net.URI;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;

import cn.dansj.common.utils.lang.Nullable;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpHead;
import org.apache.hc.client5.http.classic.methods.HttpOptions;
import org.apache.hc.client5.http.classic.methods.HttpPatch;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpPut;
import org.apache.hc.client5.http.classic.methods.HttpTrace;
import org.apache.hc.client5.http.config.Configurable;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.util.Assert;

public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory, DisposableBean {
    private HttpClient httpClient;
    @Nullable
    private BiFunction<HttpMethod, URI, HttpContext> httpContextFactory;
    private long connectTimeout = -1;
    private long connectionRequestTimeout = -1;

    public HttpComponentsClientHttpRequestFactory() {
        this.httpClient = HttpClients.createSystem();
    }

    public HttpComponentsClientHttpRequestFactory(HttpClient httpClient) {
        this.httpClient = httpClient;
    }

    public void setHttpClient(HttpClient httpClient) {
        Assert.notNull(httpClient, "HttpClient must not be null");
        this.httpClient = httpClient;
    }

    public HttpClient getHttpClient() {
        return this.httpClient;
    }

    public void setConnectTimeout(int connectTimeout) {
        Assert.isTrue(connectTimeout >= 0, "Timeout must be a non-negative value");
        this.connectTimeout = connectTimeout;
    }

    public void setConnectTimeout(Duration connectTimeout) {
        Assert.notNull(connectTimeout, "ConnectTimeout must not be null");
        Assert.isTrue(!connectTimeout.isNegative(), "Timeout must be a non-negative value");
        this.connectTimeout = connectTimeout.toMillis();
    }

    public void setConnectionRequestTimeout(int connectionRequestTimeout) {
        Assert.isTrue(connectionRequestTimeout >= 0, "Timeout must be a non-negative value");
        this.connectionRequestTimeout = connectionRequestTimeout;
    }

    public void setConnectionRequestTimeout(Duration connectionRequestTimeout) {
        Assert.notNull(connectionRequestTimeout, "ConnectionRequestTimeout must not be null");
        Assert.isTrue(!connectionRequestTimeout.isNegative(), "Timeout must be a non-negative value");
        this.connectionRequestTimeout = connectionRequestTimeout.toMillis();
    }

    public void setHttpContextFactory(BiFunction<HttpMethod, URI, HttpContext> httpContextFactory) {
        this.httpContextFactory = httpContextFactory;
    }

    @Override
    public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
        HttpClient client = getHttpClient();
        ClassicHttpRequest httpRequest = createHttpUriRequest(httpMethod, uri);
        postProcessHttpRequest(httpRequest);
        HttpContext context = createHttpContext(httpMethod, uri);
        if (context == null) context = HttpClientContext.create();
        // Request configuration not set in the context
        if (context.getAttribute(HttpClientContext.REQUEST_CONFIG) == null) {
            // Use request configuration given by the user, when available
            RequestConfig config = null;
            if (httpRequest instanceof Configurable) config = ((Configurable) httpRequest).getConfig();
            if (config == null) config = createRequestConfig(client);
            if (config != null) context.setAttribute(HttpClientContext.REQUEST_CONFIG, config);
        }
        return new HttpComponentsClientHttpRequest(client, httpRequest, context);
    }

    @Nullable
    protected RequestConfig createRequestConfig(Object client) {
        if (client instanceof Configurable) {
            RequestConfig clientRequestConfig = ((Configurable) client).getConfig();
            return mergeRequestConfig(clientRequestConfig);
        }
        return mergeRequestConfig(RequestConfig.DEFAULT);
    }

    @SuppressWarnings("deprecation")  // setConnectTimeout
    protected RequestConfig mergeRequestConfig(RequestConfig clientConfig) {
        if (this.connectTimeout == -1 && this.connectionRequestTimeout == -1) {  // nothing to merge
            return clientConfig;
        }

        RequestConfig.Builder builder = RequestConfig.copy(clientConfig);
        if (this.connectTimeout >= 0) {
            builder.setConnectTimeout(this.connectTimeout, TimeUnit.MILLISECONDS);
        }
        if (this.connectionRequestTimeout >= 0) {
            builder.setConnectionRequestTimeout(this.connectionRequestTimeout, TimeUnit.MILLISECONDS);
        }
        return builder.build();
    }

    protected ClassicHttpRequest createHttpUriRequest(HttpMethod httpMethod, URI uri) {
        if (HttpMethod.GET.equals(httpMethod)) {
            return new HttpGet(uri);
        } else if (HttpMethod.HEAD.equals(httpMethod)) {
            return new HttpHead(uri);
        } else if (HttpMethod.POST.equals(httpMethod)) {
            return new HttpPost(uri);
        } else if (HttpMethod.PUT.equals(httpMethod)) {
            return new HttpPut(uri);
        } else if (HttpMethod.PATCH.equals(httpMethod)) {
            return new HttpPatch(uri);
        } else if (HttpMethod.DELETE.equals(httpMethod)) {
            return new HttpDelete(uri);
        } else if (HttpMethod.OPTIONS.equals(httpMethod)) {
            return new HttpOptions(uri);
        } else if (HttpMethod.TRACE.equals(httpMethod)) {
            return new HttpTrace(uri);
        }
        throw new IllegalArgumentException("Invalid HTTP method: " + httpMethod);
    }

    protected void postProcessHttpRequest(ClassicHttpRequest request) {
    }

    @Nullable
    protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
        return (this.httpContextFactory != null ? this.httpContextFactory.apply(httpMethod, uri) : null);
    }

    @Override
    public void destroy() throws Exception {
        HttpClient httpClient = getHttpClient();
        if (httpClient instanceof Closeable) {
            ((Closeable) httpClient).close();
        }
    }
}
